/*
**                                nullpty.c
** ============================================================================
** This program acts as a transparant gateway for a terminal-session.
** When it is started, it allocates a pseudo-tty (master-slave pair) and starts
** a shell at the slave-side. After that this program provides a bridge
** between the real terminal and master-side of the pseudo-tty; everything
** will be passed transparantly. It can be used as basis for other
** implementations.
**
** Copyright (c) 2001 AT Computing, Nijmegen, the Netherlands.
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
** Free Software Foundation; either version 2, or (at your option) any
** later version.
**
** This program is distributed in the hope that it will be useful, but
** WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU General Public License for more details.
*/

/* needed for HP-UX */
#define _INCLUDE_HPUX_SOURCE
#define _INCLUDE_POSIX_SOURCE
#define _INCLUDE_XOPEN_SOURCE
#define _INCLUDE_XOPEN_SOURCE_EXTENDED
#define _INCLUDE_AES_SOURCE

#include <sys/types.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <signal.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <termio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/stropts.h>

#define MAXBUF 		4096

#ifndef	SIGCHLD
#define	SIGCHLD	SIGCLD
#endif

static struct termios termorig;	/* original tty-settings   */
static struct winsize winorig;	/* original tty-dimensions */

struct pst {
	char	*master;  /* name of master-pty     */
	int 	mfd;      /* file descriptor master */
	char	*slave;   /* name of slave-pty      */
	int 	sfd;      /* file descriptor slave  */
} pspair;

/*
** function proto-types
*/
static void bye(int);
static void newwinsize(int);
static void prepchild(struct pst *);
static void rawmode(int);
static int  findms(struct pst *);

int
main(int argc, char *argv[], char *environ[])
{
        int			n=1; int log_fd;
	unsigned char		iobuf[MAXBUF];
	struct sigaction 	sa;

	log_fd = open ("log.txt", O_APPEND | O_CREAT | O_RDWR);
	

	/*
	** set terminal in raw mode
	*/
	rawmode(0);

	/*
	** connect to a master/slave pseudo-tty pair;
	** keep both open to prevent unwanted streams-module pops on close.
	*/
	if ( findms(&pspair) < 0) {
		perror("open pty failed");
		bye(1); 
	}

	/*
	** fork a child to become the interactive shell
	*/
	switch (fork()) {
	   case 0:
		close(pspair.mfd);
		prepchild(&pspair);	/* and never return */
	   case -1:
		perror("fork failed");
		bye(1);
	   default:
		close(pspair.sfd);
	}

	/*
	** parent process continues here;
	** get rid of setuid-root privileges, if any
	*/
	setuid(getuid());

	/*
	** install signal-handlers for window-size change,
	** terminate and child-termination
	*/
	memset(&sa, 0, sizeof sa);
        sa.sa_handler = newwinsize;
        sa.sa_flags   = SA_RESTART;
        sigaction(SIGWINCH, &sa, (struct sigaction *)0);

	signal(SIGTERM, bye);
	signal(SIGCHLD, bye);

	/*
	** parent will pass information to both sides,
	** so acts as a gateway until EOF is received
	** from either side
	*/
	while (n > 0) {
		fd_set	readfds;

		FD_ZERO(&readfds);
		FD_SET (pspair.mfd, &readfds);
		FD_SET (0, 	    &readfds);

		/*
		** neutral state: wait for data from either side
		*/
		if ( select(pspair.mfd+1, &readfds, (fd_set *) 0,
			       (fd_set *) 0, (struct timeval *) 0) < 0) {

			if (errno==EINTR)	/* WINCH signal received ? */
				continue;

			perror("select");
			bye(1);
		}

		if ( FD_ISSET(pspair.mfd, &readfds) ) {
			if ( (n = read(pspair.mfd, iobuf, MAXBUF)) > 0) {
				if (write(1, iobuf, n) == -1) {
					perror("write stdout");
					bye(1);	
				}
				if (write(log_fd, iobuf, n) == -1) {
					perror("write stdout");
					bye(1);	
				}
			}
		}

		if ( FD_ISSET(0, &readfds) ) {
			if ( (n = read(0, iobuf, MAXBUF)) > 0) {
				if (write(pspair.mfd, iobuf, n) == -1) {
					perror("write master");
					bye(1);	
				}
				if (write(log_fd, iobuf, n) == -1) {
					perror("write master");
					bye(1);	
				}
			}
		}
	}

	bye(0); /* clean up; never come back */
}


/*
** Find master and slave pseudo-tty devices - generic version
** Master pty is accessed via the cloning device /dev/ptmx
** and corresponding child is found via standard library-routines
** unlockpt, grantpt and ptsname.
*/
static int
findms(struct pst *p)
{
	char *sname;	/* slave name pointer */

	/*
	** open ptmx: cloning device for pseudo tty's
	** for AIX: use /dev/ptc i.s.o. /dev/ptmx
	*/
	if ((p->mfd = open("/dev/ptmx", O_RDWR)) == -1) {
		if ((p->mfd = open("/dev/ptc", O_RDWR)) == -1) {
			perror("Cannot open cloning master pty");
			return -1;
		}
	}

	/*
	** unlock slave-side and give away
	*/
	(void)unlockpt(p->mfd);
	(void)grantpt (p->mfd);

	/* fetch slave-name */
	sname = ptsname(p->mfd);

	if ((p->sfd = open(sname, O_RDWR)) == -1) {
		perror("open slave pty");
		close(p->mfd);
		return -1; /* it went wrong */
	}

	/* master name not available */
	p->master = (char *)0;

	p->slave = malloc(strlen(sname)+1);
	strcpy(p->slave, sname);

	/*
	** push streams-modules ptem (fake hardware) and
	** ldterm (line discipline); these pushes may fail if
	** the pty is not streams-based (e.g. with Linux)
	** don't close sfd to prevent popping ptem and ldterm
	** 
	** for AIX the pushes will hang the process, so if the slave-fd
	** is already related to a line discipline, forget about the pushes
	*/
	if (ioctl(p->sfd, I_LIST, NULL) > 0) {	/* streams support ? */
		if (ioctl(p->sfd, I_FIND, "ldterm") != 1 &&
		    ioctl(p->sfd, I_FIND, "ldtty")  != 1   ) {
			(void) ioctl(p->sfd, I_PUSH, "ptem");
			(void) ioctl(p->sfd, I_PUSH, "ldterm");
		}
	}

	return p->mfd;
}


/*
** Prepare newly created child-process to connect to the
** slave-side of the pty and exec a shell.
*/
static void
prepchild(struct pst *pst)
{
	int	i;
	char	*shellp, *shelln;

	/*
	** close stdin, stdout and stderr which are still connected to
	** the tty from which the program is started; for some Unix-flavors
	** an explicit detach of the current controlling tty is needed first,
	** for other UNIX-flavors this will happen during the setsid later on
	**
	** do not close the slave-pty at this moment, because this pops the
	** pushed modules!
	*/
#ifdef	TIOCNOTTY	/* no implicit ctty with open */
	(void) ioctl(0, TIOCNOTTY, 0);
#endif

	for (i=0; i < 3; ++i)
		close(i);

	/*
	** establish a new session (and process group) and after that open
	** the slave-pty to make it the ctty for this session;
	** the slave-pty will be connected to fd 0, 1 and 2
	*/
	setsid();

	if ( (pst->sfd = open(pst->slave, O_RDWR)) < 0) 
	    	exit(errno);

	dup(0);
	dup(0);

#ifdef	TIOCSCTTY	/* Linux specific: no implicit ctty during open */
	(void) ioctl(0, TIOCSCTTY, 0);
#endif

	/*
	** close other inhereted fd's
	*/
	for (i=3; i < 100; ++i)
		close(i);

	/*
	** the new slave-pty should be set in the same mode as 
	** the tty from which this program has been started
	*/
	(void) ioctl(0, TCSETS,     &termorig);
	(void) ioctl(0, TIOCSWINSZ, &winorig);

	setuid(getuid()); /* drop the setuid-root privileges */

	/*
	** exec a shell in this child process: 
	** preferably use the shell defined in the environment-variable SHELL;
	** if undefined or starting problems, use ksh, bash or sh
	*/
	if ((shellp = getenv("SHELL")) != NULL) {
		if ( (shelln = strrchr(shellp, '/')) == NULL)
			shelln = shellp;
		else
			shelln++;

		execl(shellp, shelln, (char *)0);
	}

	execlp("ksh",  "ksh",  (char *)0);
	execlp("bash", "bash", (char *)0);
	execlp("sh",   "sh",   (char *)0);

	abort();	/* Should never happen */
}

/*
** Set the tty from which we are started in raw mode, so all
** data will be transparantly passed between master-pty and real tty
*/
static void
rawmode(int ttyfd)
{
	static struct termios termnew;	/* static for zero's */

	/*
	** obtain current settings of tty
	*/
	if (ioctl(ttyfd, TCGETS, &termorig) == -1) {
		perror("ioctl TCGETS failed");
		exit(1);
	}

	if (ioctl(ttyfd, TIOCGWINSZ, &winorig) == -1) {
		perror("ioctl TIOCGWINSZ failed");
		exit(1);
	}

	termnew.c_cc[VEOF]	= 1;
	termnew.c_iflag		= BRKINT|ISTRIP|IXON|IXANY;
	termnew.c_oflag		= 0;
	termnew.c_cflag		= termorig.c_cflag;
	termnew.c_lflag		= 0;

	(void) ioctl(ttyfd, TCSETS, &termnew);
}

/*
** Stop all processing.
** To be called as the catcher-routine for SIGTERM and SIGCLD/SICHLD, but
** also from other functions which want to stop the program in an orderly
** manner.
*/
static void
bye(int signum)
{
	(void) ioctl(0, TCSETS, &termorig);

	exit(signum);
}

/*
** To be called as catcher-routine for SIGWINCH-signal, which happens
** when the real tty changes its size. In that case the dimensions of the
** slave-pty have to be modified as well (which will cause a SIGWINCH-signal
** for the processes running on that tty).
*/
static void
newwinsize(int signum)
{
	int fd;

	if (ioctl(0, TIOCGWINSZ, &winorig) != -1) {
		if ((fd = open(pspair.slave, O_RDWR)) >= 0) {
			(void) ioctl(fd, TIOCSWINSZ, &winorig);
			close(fd);
		}
	}
}
